查看原文
其他

来来来!一起动手整个性能检测工具吧~

不近视的猫 郭霖 2022-12-14


/   今日科技快讯   /

苹果确定将在iOS 14.5上执行“应用追踪透明”功能。这项反追踪新功能旨在让用户掌握个人信息是否可以被应用开发者追踪的自主权,在去年6月举行的WWDC开发者大会被首度提出,是苹果持续推进隐私保护的重要规则之一。

/   作者简介   /

本篇文章来自不近视的猫同学投稿,分享了他如何写一个简易的性能检测工具,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

不近视的猫的博客地址:
https://blog.csdn.net/m0_46278918

/   前言   /

性能优化,基本每位 Android 开发都需要考虑这个问题。像LeakCanary、hugo这些都是大家常用的性能检测工具,而这次我要讲的是 BlockCanary,由于这个库很久没更新了,所以可能很多人不认识,但是这并不妨碍我们去理解它的实现原理。

/   集成步骤   /

GitHub地址:
https://github.com/markzhai/AndroidPerformanceMonitor

该库的功能主要为检测在主线程运行的操作时长,假如运行超过一定时间(默认为1000毫秒),则会记录 block 信息并提示给开发者。

加上依赖

dependencies {
    // 无论是开发版还是正式版都会检测(不建议)
    // implementation 'com.github.markzhai:blockcanary-android:1.5.0'

    // 仅在开发版时检测
    debugImplementation 'com.github.markzhai:blockcanary-android:1.5.0'
    releaseImplementation 'com.github.markzhai:blockcanary-no-op:1.5.0'
}

在自定义的 Application 的 onCreate() 方法中进行初始化。

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        BlockCanary.install(this, BlockCanaryContext()).start()
    }
}

别忘了在 AndroidMainfest.xml 中进行 Application 绑定。

android:name=".MyApplication"

/   具体检测效果   /

先看看布局效果,就一个按钮。


对按钮进行监听并点击后睡眠 2000 毫秒。

 fun clickView(view: View) {
        SystemClock.sleep(2000)
    }

点击后,BlockCanary 收集到的信息。


是不是很神奇,能够定位到阻塞的位置和阻塞的时间。

使用总结


  • 能够计算在主线程中运行方法的时长
  • 定位运行的方法在代码上的位置

所以只要解决了以上两个问题,我们就可以自己手写个简单的 BlockCanary 了。

/   具体实现   /

运行时长


em…在主线程中的运行时长…主线程…主线程…

哦!

由于 ActivityThread 被 MainLooper 一直死循环,所以在主线程运行操作基本都是通过 post 消息到 MessageQueue 中,由 MainLooper 取出并执行,那我们可以在执行前记录时间 A,在执行后也记录时间 B,B - A 得到的便是运行时长!

好,我们来看看 Looper 的 loop() 源码:

public static void loop() {
        final Looper me = myLooper();
         ···
        for (;;) {
            ···
            final Printer logging = me.mLogging;
            if (logging != null) {
                 logging.println(">>>>> Dispatching to " + msg.target + " " +
                 msg.callback + ": " + msg.what);
            }
            ···
            msg.target.dispatchMessage(msg);
            ···
            if (logging != null) {
                 logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
            ···
        }
   }

这不就是我们想要的吗,在执行方法前进行输出,在执行方法后再进行输出。我们来看看 me.mLogging 是怎么赋值的。me 调用 myLooper() 获取。

 public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

所以 mLogging 其实就是当前 Looper 的 mLogging。

private Printer mLogging;

    public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }

OK,思路我们有了,只要调用 setMessageLogging 进行赋值,并重写 Printer 的 println() 方法,到时 Looper 就会自动调用我们的 println() 方法了。完整代码如下所示:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initPrinter()
    }

    //使用 printerStart 进行判断是方法执行前还是执行后调用 println
    private var printerStart = true
    //记录方法执行前的时间
    private var printerStartTime = 0L

    private fun initPrinter() {
        Looper.getMainLooper().setMessageLogging {
            if (printerStart) {
                printerStart = false
                printerStartTime = System.currentTimeMillis();
            } else {
                printerStart = true
                Log.i("Printer", "方法运行的总时长:${System.currentTimeMillis() - printerStartTime}")
            }
        }
    }

    fun clickView(view: View) {
        SystemClock.sleep(2000)
    }
}

运行 App,点击按钮:

com.example.testblockcanary I/Printer: 方法运行的总时长:1
com.example.testblockcanary I/Printer: 方法运行的总时长:3
com.example.testblockcanary I/Printer: 方法运行的总时长:3
com.example.testblockcanary I/Printer: 方法运行的总时长:4
com.example.testblockcanary I/Printer: 方法运行的总时长:3
com.example.testblockcanary I/Printer: 方法运行的总时长:2009

可以看到,只要在主线程执行方法的时候,都会调用我们的 println() 方法,但是,我们只是为了检测耗时较长的方法即可,所以多加一个拦截:

 //方法执行时间超过该值才输出
    private val minTime = 1000L
    private fun initPrinter() {
    Looper.getMainLooper().setMessageLogging {
            if (printerStart) {
                printerStart = false
                printerStartTime = System.currentTimeMillis();
            } else {
                printerStart = true
                (System.currentTimeMillis() - printerStartTime).let {
                    if (it >= minTime){
                        Log.i("Printer", "方法运行的总时长:${it}")
                    }
                }
            }
        }
    }

运行后,点击:

com.example.testblockcanary I/Printer: 方法运行的总时长:2002

第一部分功能完成了,我们可以开始第二部分了。

/   定位代码   /

em…这块代码我是从 BlockCanary 源码搬过来进行调整后,由于就是一些 SDK 方法的调度,能讲解的不多:

private fun initPrinter() {
        Looper.getMainLooper().setMessageLogging {
            if (printerStart) {
                printerStart = false
                printerStartTime = System.currentTimeMillis();
            } else {
                printerStart = true
                (System.currentTimeMillis() - printerStartTime).let {
                    if (it >= minTime){
                        Log.i("Printer", "方法运行的总时长:${it}")
                        //--------------------------------------------//
                        //获取栈信息进行输出
                        val stringBuilder = StringBuilder()
                        for (stackTraceElement in Looper.getMainLooper().getThread().getStackTrace()) {
                            stringBuilder
                                .append(stackTraceElement.toString())
                                .append(BlockInfo.SEPARATOR)
                        }
                        Log.i("Printer", "StackTrace:${stringBuilder.toString()}")
                        //--------------------------------------------//
                    }
                }
            }
        }
    }

运行,点击后的效果:

com.example.testblockcanary I/Printer: 方法运行的总时长:2006
com.example.testblockcanary I/Printer: StackTrace:java.lang.System.currentTimeMillis(Native Method)
    com.example.testblockcanary.MainActivity$initPrinter$1.println(MainActivity.kt:31)
    android.os.Looper.loop(Looper.java:145)
    android.app.ActivityThread.main(ActivityThread.java:6077)
    java.lang.reflect.Method.invoke(Native Method)
    com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
    com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)

由上可以看出,确实是输出了当前的线程的栈信息。不过,仔细看下,不太对劲啊,为什么没有定位到 clickView() 方法,不是它阻塞的吗?想想也正常,因为第二次调用 println() 方法的时候,clickView() 已经执行完出栈了,自然不会输出 clickView() 的信息,所以,我们必须在 clickView() 执行的时候就要输出栈信息,也就是在 minTime 时间前,我这里选取的时间为 minTime * 0.8。怕你们忘了 minTime 是什么,提示下:

//方法执行时间超过该值才输出
private val minTime = 1000L

所以,我们在 printerStart 为 true 的时候,就使用 handler 发送一个 minTime * 0.8 的延迟消息,用于记录此时栈信息,假如 printerStart 为 false 的时候,执行总时间短于 minTime,我们就不输出,否则就输出栈信息。

初始化Handler

private lateinit var delayHandler: Handler
private fun initDelayHandler() {
    //让Handler的消息在子线程中运行
    val handlerThread = HandlerThread("DelayThread")
    handlerThread.start()

    delayHandler = Handler(handlerThread.looper)
}

将获取栈消息功能单独抽取出来。

 private val stringBuilder = StringBuilder()
    private val runnable = Runnable {
        //获取栈信息进行记录
        for (stackTraceElement in Looper.getMainLooper().getThread().getStackTrace()) {
            stringBuilder
                .append(stackTraceElement.toString())
                .append(BlockInfo.SEPARATOR)
        }

    }

runnable 的发送和销毁。

 private fun initPrinter() {
        Looper.getMainLooper().setMessageLogging {
            if (printerStart) {
                printerStart = false
                printerStartTime = System.currentTimeMillis();
                delayHandler.removeCallbacks(runnable)
                //延迟minTime * 0.8发送,用于记录阻塞时的栈信息
                delayHandler.postDelayed(runnable, (minTime * 0.8).toLong())
            } else {
                printerStart = true
                delayHandler.removeCallbacks(runnable)
                (System.currentTimeMillis() - printerStartTime).let {
                    if (it >= minTime) {
                        Log.i("Printer", "方法运行的总时长:${it}")
                        Log.i("Printer", "StackTrace:${stringBuilder.toString()}")
                    }
                }
            }
        }
    }

运行,点击效果:

com.example.testblockcanary I/Printer: 方法运行的总时长:2011
com.example.testblockcanary I/Printer: StackTrace:java.lang.Thread.sleep(Native Method)
    java.lang.Thread.sleep(Thread.java:371)
    java.lang.Thread.sleep(Thread.java:313)
    android.os.SystemClock.sleep(SystemClock.java:120)
    com.example.testblockcanary.MainActivity.clickView(MainActivity.kt:67)
    java.lang.reflect.Method.invoke(Native Method)
    androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:409)
    android.view.View.performClick(View.java:5610)
    android.view.View$PerformClick.run(View.java:22265)
    android.os.Handler.handleCallback(Handler.java:751)
    android.os.Handler.dispatchMessage(Handler.java:95)
    android.os.Looper.loop(Looper.java:154)
    android.app.ActivityThread.main(ActivityThread.java:6077)
    java.lang.reflect.Method.invoke(Native Method)
    com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
    com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)

终于输出正确的栈信息了!

另外,这里再提个额外的信息。在新版的 LeakCanary 中,只要进行依赖,不需要在 Application 中进行初始化就可以直接使用了!

既然我们也是要写个性能检测工具,那我们也可以参考该做法进行实现。其实,就是在 ContentProvider 的 onCreate() 方法中进行初始化,因为 App 在启动的时候,是优先初始化 ContentProvider 再初始化 Application 的。具体代码我就不贴了,不难,只是给还未了解到这块的同学多传输些信息。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
Compose Android开发终极挑战赛: 写一个天气应用
啥也不说了,Room真香!

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存